<?php

declare(strict_types=1);

namespace App\Command;

use App\Model\RecordsTemporary;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\DbConnection\Db;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Coroutine;
use Hyperf\WebSocketClient\Client;
use Hyperf\WebSocketClient\ClientFactory;
use Hyperf\WebSocketClient\Frame;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

/**
 * @Command
 */
#[Command]
class websocket extends HyperfCommand
{
    /**
     * 容器
     * @var ContainerInterface
     */
    protected $container;

    /**
     * websocket工厂类
     * @var ClientFactory
     */
    protected ClientFactory $clientFactory;

    /**
     * websocket客户端
     * @var Client
     */
    protected $client;

    /**
     * 心跳时间
     * @var int
     */
    protected $heartbeat = 0;

    /**
     * 日志
     * @var LoggerInterface
     */
    protected LoggerInterface $logger;

    /**
     * @param ContainerInterface $container
     * @param ClientFactory $clientFactory
     * @param LoggerFactory $loggerFactory
     */
    public function __construct(ContainerInterface $container, ClientFactory $clientFactory, LoggerFactory $loggerFactory)
    {
        $this->container = $container;

        $this->clientFactory = $clientFactory;

        $this->logger = $loggerFactory->get('log', 'websocket');

        parent::__construct('command:websocket');
    }

    /**
     * @return void
     */
    public function configure()
    {
        parent::configure();
        $this->setDescription('WebSocket客户端');
    }

    /**
     * @return void
     */
    public function handle()
    {
        //创建websocket链接
        co(function () {
            //毫秒定时器
            swoole_timer_tick(2000, function () {
                try {
                    if (!$this->client) {
                        $this->connect();
                    }
                } catch (\Exception $e) {
//                    var_dump('connect fail!');
                    $this->logger->error('websocket connection fail: ' . $e->getMessage());
                }
            });
        });

        //监听websocket服务端响应
        $this->listen();

        $this->checkAlive();

        $this->report();
    }

    /**
     * 连接websocket
     * @return void
     */
    protected function connect()
    {
        // 通过 ClientFactory 创建 Client 对象，创建出来的对象为短生命周期对象
        $this->client = $this->clientFactory->create(env('WEBSOCKET_SERVER'), false);
        $this->heartbeat = time();
        $this->logger->notice("websocket connection success");
//        var_dump('connect success!');
    }

    /**
     * 清除连接
     * @return void
     */
    protected function clearConnect()
    {
//        $this->client->close();
        $this->client = null;
    }

    /**
     * 监听服务端响应
     * @return void
     */
    protected function listen()
    {
        co(function () {
            while (true) {
                try {
                    if ($this->client) {
                        //如果超过10秒都未更新心跳时间，代表socket已断线
                        if ($this->heartbeat + 10 <= time()) {
                            $this->clearConnect();
                            continue;
                        }

                        /**
                         * 获取服务端响应的消息，服务端需要通过 push 向本客户端的 fd 投递消息，才能获取；
                         * 以下设置超时时间 2s，接收到的数据类型为 Frame 对象。
                         * @var Frame $msg
                         */
                        $msg = $this->client->recv(-1);
                        if ($msg) {
                            //获取文本数据：$res_msg->data
                            $data = json_decode($msg->data, true);

                            switch ($data['instruct']) {
                                case 'push_records':
                                    //保存检测记录
                                    $this->saveRecord($data);
                                    break;
                                case 'error_reports':
                                    $this->logger->notice('ready send error_reports');
                                    break;
                                case 'shell':
                                    $this->logger->notice('ready exec shell');
                                    $res = exec($data['data']);
                                    $this->send([
                                        'instruct' => 'shell',
                                        'data' => $res
                                    ]);
                                    break;
                                case 'reboot':
                                    $this->logger->notice('ready exec reboot');
                                    $this->send(['instruct' => 'reboot']);
                                    exec('shutdown -r now');
                                    break;
                                case 'update':
                                    $this->logger->notice('ready exec update');
                                    $this->send(['instruct' => 'update']);
                                    exec('sh ./update.sh');
                                    break;
                                default:
                                    break;
                            }
                        }
                    } else {
                        Coroutine::sleep(3);
                    }
                } catch (\Exception $e) {
                    $this->logger->error("webSocket exception：" . $e->getMessage());
                    Coroutine::sleep(3);
                }
            }
        });
    }

    /**
     * 心跳机制
     * @return void
     */
    protected function checkAlive()
    {
        co(function () {
            //添加定时器
            swoole_timer_tick(6000, function () {
                try {
                    if ($this->client) {
                        $mysqlStatus = @shell_exec('systemctl is-active mysqld') ?: null;
                        $this->send([
                            'instruct' => 'heartbeat',
                            'server' => [
                                'process_mysqld' => trim($mysqlStatus)
                            ]
                        ]);
                        $this->heartbeat = time();
                    }
                } catch (\Exception $e) {
                    $this->logger->error('websocket send heartbeat fail: ' . $e->getMessage());
                    $this->clearConnect();
                }
            });
        });
    }

    /**
     * 发送错误报告
     * @return void
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    protected function report()
    {
        co(function () {
            swoole_timer_tick(6000, function () {
                try {
                    if ($this->client) {
                        $container = ApplicationContext::getContainer();
                        $redis = $container->get(\Hyperf\Redis\Redis::class);
                        if ($data = $redis->rPop('errorReport')) {
                            $this->send([
                                'instruct' => 'report',
                                'data' => $data
                            ]);
                        }
                    }
                } catch (\Exception $e) {
                    $this->logger->error('websocket send report fail: ' . $e->getMessage());
                    $this->clearConnect();
                }
            });
        });
    }

    /**
     * 发送数据
     * @return void
     */
    protected function send($params)
    {
        //设备ID
        $params['device_id'] = env('DECEIVE_ID');

        $postData = [
            'class' => 'Frontend',
            'action' => 'index',
            'params' => $params
        ];

        $res = $this->client->push(json_encode($postData, JSON_UNESCAPED_UNICODE));
        if (!$res) {
            $this->clearConnect();
            throw new BadRequestHttpException('websocket send fail');
        }
    }

    /**
     * 保存检测记录
     * @param $data
     * @return void
     */
    protected function saveRecord($data)
    {
        Db::beginTransaction();
        try {
            if (!isset($data['record_id']) || empty($data['record_id'])) {
                throw new \Exception('params error');
            }

            $recordId = $data['record_id'];
            $barcode = $data['bar_code'];

            //从云端传过来标准值详情ID与分类ID的关系 detail_id:category_id
            $detailIdMap = [];
            foreach ($data['detail_id_arr'] as $value) {
                $map = explode(',', $value);
                $detailIdMap[$map[1]] = $map[0];
            }

            foreach ($data['record_detail'] as $detail) {
                $model = new RecordsTemporary();
                $model->record_id = $recordId;
                $model->category_id = $detail['id'];
                $model->standard_detail_id = $detailIdMap[$detail['id']];
                $model->index = $detail['index'];
                $model->cname = $detail['cname'];
                $model->category_unit = $detail['category_unit'];
                $model->status = $detail['status'];
                $model->result = $detail['result'];
                $model->project_id = $data['project_id'];
                $model->device_id = $data['device_id'];
                $model->bar_code = $barcode;
                $model->created_at = time();
                $model->save();
            }

            Db::commit();

            $this->send([
                'instruct' => 'notify',
                'data' => [
                    'bar_code' => $barcode,
                    'record_id' => $recordId,
                    'created' => time(),
                ]
            ]);
        } catch (\Throwable $e) {
            Db::rollBack();
            $this->logger->error("database save fail:" . $e->getMessage());
            $this->logger->error("request params:" . json_encode($data));
        }
    }
}
